﻿using EnvDTE;
using System;
using System.Collections.Generic;
using EnvDTE80;
using ClassMetadataBrowser.Domain;
using System.Linq;
using Thread = System.Threading.Thread;
using System.Threading;
using System.IO;

namespace ClassMetadataBrowser
{
	internal class MetadataController : IMetadataProvider, IDisposable
	{
		public event EventHandler Rebind;
		public event EventHandler Initialized;
		public event EventHandler ActiveWindowChanged;

		#region class members

		private readonly string[] _allowedExtenstions = new string[] { ".cs", ".vb" };
		private readonly DTE2 _dte;
		private readonly List<MetadataObjectBase> _metadata;
		private readonly INavigateToCodeElement _navigator;
		private MetadataFilesWatcher _watcher;
		private bool _isAvailable;
		private bool _isInitialized;
		private Thread _th;
		private ProjectItem _interstingPi;
		private WindowEvents _events;
		private string _lastDocName;

		#endregion

		#region properties

		public string SlnFilePath
		{
			get
			{
				if (_dte.Solution.IsOpen)
					return _dte.Solution.FullName;

				return string.Empty;
			}
		}

		public string ActiveWindowName
		{
			get;
			private set;
		}

		public IList<MetadataObjectBase> Metadata
		{
			get { return _metadata; }
		}

		public bool IsAvailable
		{
			get { return _isAvailable; }
		}

		public bool IsInitialized
		{
			get { throw new NotImplementedException(); }
		}

		#endregion

		public MetadataController(DTE2 dte, INavigateToCodeElement navigator, WindowEvents events)
		{
			_navigator = navigator;
			_dte = dte;
			_metadata = new List<MetadataObjectBase>();
			_watcher = new MetadataFilesWatcher(Path.GetDirectoryName(dte.Solution.FileName), this);

			_events = events;
			_events.WindowActivated += new _dispWindowEvents_WindowActivatedEventHandler(WindowActivated);

			if (_dte.ActiveDocument != null && DoesFileMatch(_dte.ActiveDocument.FullName))
			{
				_lastDocName = _dte.ActiveDocument.FullName;
				Initialize(_dte.ActiveDocument.ProjectItem);
			}
		}

		private void WindowActivated(Window gotFocus, Window lostFocus)
		{
			if (gotFocus.Document == null || gotFocus.Document.FullName.Equals(_lastDocName))
				return;
			if (gotFocus.Document != null)
				_metadata.Clear();
			if (gotFocus.Document != null)
			{
				_lastDocName = gotFocus.Document.FullName;
				Initialize(gotFocus.Document.ProjectItem);
				ActiveWindowName = Path.GetFileName(_lastDocName);
				ActiveWindowChanged(this, EventArgs.Empty);
			}

			if (Rebind != null)
				Rebind(this, EventArgs.Empty);
		}

		private void Initialize(ProjectItem element)
		{
			_isInitialized = true;
			_interstingPi = element;

			if (DoesFileMatch(element.Name))
			{
				AsyncInitialize();
				//_th = new Thread(AsyncInitialize);
				//_th.Priority = ThreadPriority.Normal;
				//_th.Start();
			}
			else
			{
				_isAvailable = true;
			}
		}

		public void AbortThread()
		{
			if (_th != null && _th.IsAlive)
				_th.Abort();
		}

		private void AsyncInitialize()
		{
			if (!_dte.Solution.IsOpen || _interstingPi.FileCodeModel == null)
				return;

			_metadata.Clear();
			try
			{
				_metadata.AddRange(Collect(_interstingPi.FileCodeModel.CodeElements, _interstingPi));
				_watcher.Dispose();
				_watcher = new MetadataFilesWatcher(this);
				_isAvailable = true;
				_watcher.Start(_interstingPi.Document.FullName);
				if (Initialized != null)
					Initialized(this, EventArgs.Empty);
			}
			catch (Exception ex)
			{
				// todo(0): file might be corrupted
			}
		}

		private IList<MetadataObjectBase> Collect(CodeElements elements, ProjectItem item)
		{
			List<MetadataObjectBase> objs = new List<MetadataObjectBase>();

			foreach (CodeElement element in elements)
			{
				MetadataObjectType objectType = GetObjectType(element.Kind);
				if (objectType != MetadataObjectType.Uknown)
				{
					MetadataObjectBase obj = MetadataObjectBase.CreateCode(objectType, element, element.FullName, _navigator);
					obj.ContainingFilePath = item.Name;
					objs.Add(obj);
				}
				if (element.Children != null && element.Children.Count > 0)
					objs.AddRange(Collect(element.Children, item));
			}

			return objs;
		}

		#region dirty routines to work with dte objects

		private MetadataObjectType GetObjectType(vsCMElement kind)
		{
			// mapping classes
			switch (kind)
			{
				case vsCMElement.vsCMElementClass:
					return MetadataObjectType.Class;
				case vsCMElement.vsCMElementDelegate:
					return MetadataObjectType.Delegate;
				case vsCMElement.vsCMElementEnum:
					return MetadataObjectType.Enum;
				case vsCMElement.vsCMElementEvent:
					return MetadataObjectType.Event;
				case vsCMElement.vsCMElementFunction:
					return MetadataObjectType.Method;
				case vsCMElement.vsCMElementProperty:
					return MetadataObjectType.Property;
				case vsCMElement.vsCMElementStruct:
					return MetadataObjectType.Struct;
				case vsCMElement.vsCMElementInterface:
					return MetadataObjectType.Interface;
				default:
					return MetadataObjectType.Uknown;
			}
		}

		private bool DoesFileMatch(string fileName)
		{
			foreach (string ext in _allowedExtenstions)
				if (fileName.EndsWith(ext))
					return true;

			return false;
		}

		private IList<Project> RetriveSolutionProjects()
		{
			IList<Project> rootProjects = new List<Project>();
			IList<Project> allProjets = new List<Project>();

			for (int projectIndex = 1; projectIndex <= _dte.Solution.Projects.Count; ++projectIndex)
				rootProjects.Add(_dte.Solution.Projects.Item(projectIndex));
			GetProjects(allProjets, rootProjects);
			return allProjets;
		}

		private void GetProjects(IList<Project> collection, IList<Project> projects)
		{
			foreach (Project project in projects)
			{
				if (project.Kind == Constants.vsProjectKindSolutionItems)
				{
					List<Project> innerItems = new List<Project>();
					for (int projectItem = 1; projectItem <= project.ProjectItems.Count; ++projectItem)
					{
						Project proj = project.ProjectItems.Item(projectItem).Object as Project;
						if (proj != null)
							innerItems.Add(proj);
						GetProjects(collection, innerItems);
					}
				}
				else
					collection.Add(project);
			}
		}

		#endregion

		#region IMetadataProvider Members

		public void ReInitializeMetadata(string currentCodeFilePath, string newCodeFilePath)
		{
			_metadata.RemoveAll(delegate(MetadataObjectBase obj)
			{
				if (obj.ContainingFilePath == currentCodeFilePath)
					return true;
				else
					return false;
			});

			if (Rebind != null)
				Rebind(this, EventArgs.Empty);
		}

		#endregion

		#region IDisposable Members

		public void Dispose()
		{
			_events = null;
			_metadata.Clear();
			_watcher.Dispose();
		}

		#endregion

		public IList<ProjectFile> GetApplicableProjectItems(string[] applicableExtensions)
		{
			IList<Project> projects = RetriveSolutionProjects();
			List<ProjectFile> files = new List<ProjectFile>();

			foreach (Project project in projects)
				PopulateFiles(files, project.ProjectItems, project.Name, applicableExtensions);

			return files;
		}

		public void PopulateFiles(List<ProjectFile> files, ProjectItems items, 
			string projectName, string [] extensions)
		{
			if (items == null)
				return;

			for (int projectItemIndex = 0; projectItemIndex < items.Count; ++projectItemIndex)
			{
				ProjectItem item = items.Item(projectItemIndex + 1);

				bool validFile = false;
				if (item == null)
					continue;

				if (item.ProjectItems != null && item.ProjectItems.Count > 0)
				{
					PopulateFiles(files, item.ProjectItems, projectName, extensions);

					// skipping folders
					if (item.Kind == Constants.vsProjectItemKindPhysicalFolder
						|| item.Kind == Constants.vsProjectItemKindSolutionItems
						|| item.Kind == Constants.vsProjectItemKindVirtualFolder)
						continue;
				}

				foreach (string validExt in extensions)
					if (item.Name.EndsWith(validExt) && !string.IsNullOrEmpty(validExt))
						validFile = true;

				if (validFile)
				{
					ProjectFile projectFile = new ProjectFile()
					{
						Name = item.Name,
						ProjectName = projectName,
						ProjectItem = item,
					};

					try
					{
						projectFile.FilePath = item.get_FileNames(0);
					}
					catch { // on some file types it erros out. in next release have to get rid of this empty try catch 
					}

					files.Add(projectFile);
				}
			}
		}

		public void ExecuteSelectionCommand()
		{
		}
	}
}
